<?php
/**
 * AlfaLoader Class
 * Singleton. Manages class/interface retrieval, caching and inclusion.
 */
class AlfaLoader {
	private static $theInstance;

	/**
	 * Class Directories
	 * Holds the directories AlfaLoader scans for class/interface definitions
	 * @var array
	 * @access private
	 */
	private $classDirectories = array();

	/**
	 * Cache File Path
	 *
	 * Holds the filename of the generated cache file.
	 *
	 * @var string
	 * @access private
	 */
	private $cacheFilename = 'alfaloader_cache.php';

	/**
	 * Class Index
	 * Holds an associative array (class name => class file) when scanning.
	 * @var array
	 * @access private
	 */
	private $classIndex = array();

	/**
	 * Class File Endings
	 * Files with these endings will be parsed by the class/interface scanner.
	 * @var array
	 * @access private
	 */
	private $classFileEndings = array();


	/**
	 * The regular expression to use when parsing for classes
	 */
	public $classRegularExpression =  "%(interface|class)\s+(\w+)\s+(extends\s+(\w+)\s+)?(implements\s+\w+\s*(,\s*\w+\s*)*)?{%";

	/**
	 * Constructor
	 * Initialize AlfaLoader
	 * @access private
	 */
	private function __construct() {
		/* do something smart here */
	}

	/**
	 * Set Cache File Path
	 * Define a path to store the cache file. Make sure PHP has permission read/write on it.
	 * @access public
	 * @param string string Cache File Path
	 */
	public function setCacheFilename($cacheFilename) {
		$this->cacheFilename = $cacheFilename;
	}

	/**
	 * Set Class File Endings
	 *
	 * Define which file endings will be considered by the class/interface scanner
	 * An empty array will let the scanner parse any file type.
	 *
	 * @access public
	 * @param array Endings
	 */
	public function setClassFileEndings($classFileEndings) {
		$this->classFileEndings = $classFileEndings;
	}

	/**
	 * Add a directory to retrieve classes/interfaces from
	 *
	 * This function adds a directory to retrieve class/interface definitions from.
	 *
	 * @access public
	 * @param string $directory_path
	 */
	public function addDir($directory_path) {
		// add trailing slash
		if(substr($directory_path, -1) != '/') {
			$directory_path .= '/';
		}
		if(!in_array($directory_path, $this->classDirectories)) {
			$this->classDirectories[] = $directory_path;
		}
	}

	/**
	 * Static factory method
	 * @access public
	 */
	public static function getInstance() {
		if (!self::$theInstance) {
			self::$theInstance = new AlfaLoader();
		}
		return self::$theInstance;
	}

	/**
	 * Gets called, when an undefined class is being instantiated
	 *
	 * @param_string string class name
	 */
	public static function autoload($class_name) {
		$config = AlfaConfiguration::getInstance();

		/* initializing loader */
		if (!self::$theInstance) {
			self::$theInstance = new AlfaLoader();

			self::$theInstance->setCacheFilename($config->getString('cache/dir') . "/" . $config->getString('loader/cache-file'));

			// include core classes
			self::$theInstance->addDir(CORE_APP_REAL_PATH . 'src/main/classes');

			// include extra classes
			$baseDir = $config->getString('appRootDir');
			$includePaths = $config->get('loader/include/dir');
			foreach ($includePaths as $dirElement) {
				$dir = (string)$dirElement;
				self::$theInstance->addDir($baseDir . '/' . $dir);
			}

			self::$theInstance->setClassFileEndings(array('php'));
		}

		/* load the class or trigger some fatal error on failure */
		try {
			if(!self::$theInstance->loadClass($class_name, $config->getBoolean('webapp/devMode', false))) {
				throw new Exception("AlfaLoader: Cannot load class '".$class_name."'", E_USER_ERROR);
			}
		} catch (Exception $e) {
			// do nothing here, pass it to the next loader
			//			AlfaApplication::exceptionHandler($e);
		}
	}


	/**
		* Load a Class
		*
		* Loads a class by its name
		* - If the matching class definition file can't be found in the cache,
		* 	it will try once again with $retry = true.
		* - When retrying, the cached index is invalidated, regenerated and re-included.
		*
		* @access public
		* @param string $class_name
		* @param boolean $retry used for recursion
		* @return boolean Success
		*/
	public function loadClass($class_name, $retry = false) {
		/* Is the class already defined? (can be omitted in combination with __autoload) */
		if (class_exists($class_name)) {
			return true;
		}

		/* Is our cache outdated or not available? Recreate it! */
		if($retry || !is_readable($this->cacheFilename)) {
			$this->createCache();
		}

		/* Include the cache file or raise error if something's wrong with the cache */
		if (!include_once($this->cacheFilename)) {
			trigger_error("AlfaLoader: Cannot include cache file from '".$this->cacheFilename."'", E_USER_ERROR);
		}

		/* Include requested file. Return on success */
		if (isset($GLOBALS['alfaloader_classes'][$class_name]) && is_readable($GLOBALS['alfaloader_classes'][$class_name])) {
			if (include_once($GLOBALS['alfaloader_classes'][$class_name])) {
				return true;
			}
		}

		/* On failure retry recursively, but only once. */
		if ($retry) {
			return false;
		} else {
			return $this->loadClass($class_name, true);
		}
	}


	/**
	 * Returns a list with all cached classes
	 */
	public function listClasses() {
		/* Is our cache outdated or not available? Recreate it! */
		if(!is_readable($this->cacheFilename)) {
			$this->createCache();
		}

		if (!isset($GLOBALS['alfaloader_classes'])) {
			/* Include the cache file or raise error if something's wrong with the cache */
			if (!include_once($this->cacheFilename)) {
				trigger_error("AlfaLoader: Cannot include cache file from '".$this->cacheFilename."'", E_USER_ERROR);
			}
		}

		return $GLOBALS['alfaloader_classes'];
	}

	/**
		* Create Cache
		*
		* - Scans the class dirs for class/interface definitions and
		* 	creates an associative array (class name => class file)
		* - Generates the array in PHP code and saves it as cache file
		*
		* @access private
		* @param param_type $param_name
		*/
	private function createCache() {
		/* Create class list */
		foreach($this->classDirectories as $dir) {
			$this->parseDir($dir);
		}

		/* Generate php cache file */
		$cache_content = "<?php\n\t// this is a automatically generated cache file.\n"
		."\t// it serves as 'class name' / 'class file' association index for the AlfaLoader\n";
		foreach($this->classIndex as $class_name => $class_file) {
			$cache_content .= "\t\$GLOBALS['alfaloader_classes']['".$class_name."'] = '".$class_file."';\n";
		}
		$cache_content .= "?>";
		if($cacheFilename_handle = fopen($this->cacheFilename, "w+")) {
			fwrite($cacheFilename_handle, $cache_content);
			/* Allow ftp users to access/modify/delete cache file, suppress chmod errors here */
			@chmod($this->cacheFilename, 0664);
		}
	}

	/**
		* Parse Directory
		*
		* Parses a directory for class/interface definitions. Saves found definitions
		* in $classIndex.
		*
		* @access private
		* @param string $directory_path
		* @return boolean Success
		*/
	private function parseDir($directory_path) {
		$files  = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory_path),RecursiveIteratorIterator::SELF_FIRST);
		foreach ($files as $file) {
			if ($file->isFile() && in_array(pathinfo($file->getFilename(), PATHINFO_EXTENSION), $this->classFileEndings)) {
				$fullFilename = $file->getPath() . "/" . $file->getFilename();
				$fullFilename = str_replace('\\', '/', $fullFilename);

				$lines = file($fullFilename);
				foreach ($lines as $line_num => $line) {
					$result = array();
					if(($res = preg_match_all( $this->classRegularExpression, $line, $result )) != FALSE ) {
						if (!isset ($result[2]) || !is_array($result[2])) {
							// this file contains no classes
						} else {
							foreach($result[2] as $class_name) {
								$this->classIndex[$class_name] = $fullFilename;
							}
						}
					}
				}
			}
		}
	}

}
?>